package cc.rengu.redp.base.xss;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * XSS过滤处理
 * <p>
 * 一般情况下，我们都是通过request的parameter来传递参数。
 * 但是，如果在某些场景下面，通过requestBody体(json等)，来传递相应参数应该怎么办？
 * 这就要需要我们对request的inputStream来进行来过滤处理了
 * <p>
 * 有个地方需要注意一下的:
 * servlet中inputStream只能一次读取，后续不能再次读取inputStream。Xss过滤器中读取了stream之后，
 * 后续如果其他逻辑涉及到inputStream读取，会抛出异常。那我们就需要想办法把已经读取的stream，重新放回到请求中。
 *
 * @Author:
 * @Date: 2021/1/27 17:11
 */
@Slf4j
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    HttpServletRequest orgRequest;

    String encoding;

    HtmlFilter htmlFilter;

    private final static String JSON_CONTENT_TYPE = "application/json";

    private final static String CONTENT_TYPE = "Content-Type";


    /**
     * @param request     HttpServletRequest
     * @param encoding    编码
     * @param excludeTags 例外的特定标签
     * @param includeTags 需要过滤的标签
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request, String encoding, List<String> excludeTags, List<String> includeTags) {
        super(request);
        orgRequest = request;
        this.encoding = encoding;
        this.htmlFilter = HtmlFilter.create(excludeTags, includeTags);
    }

    /**
     * @param request  HttpServletRequest
     * @param encoding 编码
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request, String encoding) {
        this(request, encoding, null, null);
    }

    private String xssFilter(String input) {

        log.debug("XSS处理前：{}",input);
        if (input == null || input.isEmpty() || input.trim().equals("")) {
            return "";
        } else {
//            input =  htmlFilter.clean(input);
            input = stripXSSAndSql(input);
        }

        String content = "";
        content = htmlFilter.clean(input);
        content = content.replace("&nbsp", "");//将里面的空格转义符号为空串
        content = content.replace(">", "＞").replace("<", "＜").replace("./", "．／").replace("../", "．．／");

        log.debug("XSS处理后：{}",content);
        return content;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        // 非json处理
        if (!JSON_CONTENT_TYPE.equalsIgnoreCase(super.getHeader(CONTENT_TYPE))) {
            return super.getInputStream();
        }
        InputStream in = super.getInputStream();
        String body = IOUtils.toString(in, encoding);
        IOUtils.closeQuietly(in);

        //空串处理直接返回
        if (StringUtils.isEmpty(body)) {
            return super.getInputStream();
        }

        // xss过滤
        body = xssFilter(body);
        return new RequestCachingInputStream(body.getBytes(encoding));

    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(xssFilter(name));
        if (!StringUtils.isEmpty(value)) {
            value = xssFilter(value);
        }
        return value;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] parameters = super.getParameterValues(name);
        if (parameters == null || parameters.length == 0) {
            return null;
        }

        for (int i = 0; i < parameters.length; i++) {
            parameters[i] = xssFilter(parameters[i]);
        }
        return parameters;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> map = new LinkedHashMap<>();
        Map<String, String[]> parameters = super.getParameterMap();
        for (String key : parameters.keySet()) {
            String[] values = parameters.get(key);
            for (int i = 0; i < values.length; i++) {
                values[i] = xssFilter(values[i]);
            }
            map.put(key, values);
        }
        return map;
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(xssFilter(name));
        if (!StringUtils.isEmpty(value)) {
            value = xssFilter(value);
        }
        return value;
    }

    /**
     * <b>
     * #获取最原始的request
     * </b>
     */
    public HttpServletRequest getOrgRequest() {
        return orgRequest;
    }

    /**
     * <b>
     * #获取最原始的request
     * </b>
     *
     * @param request HttpServletRequest
     */
    public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
        if (request instanceof XssHttpServletRequestWrapper) {
            return ((XssHttpServletRequestWrapper) request).getOrgRequest();
        }
        return request;
    }

    /**
     * <pre>
     * servlet中inputStream只能一次读取，后续不能再次读取inputStream
     * xss过滤body后，重新把流放入ServletInputStream中
     * </pre>
     */
    private static class RequestCachingInputStream extends ServletInputStream {
        private final ByteArrayInputStream inputStream;

        public RequestCachingInputStream(byte[] bytes) {
            inputStream = new ByteArrayInputStream(bytes);
        }

        @Override
        public int read() throws IOException {
            return inputStream.read();
        }

        @Override
        public boolean isFinished() {
            return inputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
        }

    }

    /**
     * 防止xss跨脚本攻击（替换，根据实际情况调整）,此处均为必需过滤内容，后续有新增再增加正则校验规则。
     */
    public static String stripXSSAndSql(String value) {
        if (value != null) {
            // NOTE: It's highly recommended to use the ESAPI library and
            // uncomment the following line to avoid encoded attacks.
            // value = ESAPI.encoder().canonicalize(value);
/**            value = value.replaceAll("", "");***/
            // Avoid anything between script tags
            Pattern scriptPattern = Pattern.compile("<[\r\n| | ]*script[\r\n| | ]*>(.*?)</[\r\n| | ]*script[\r\n| | ]*>", Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid anything in a src="http://www.yihaomen.com/article/java/..." type of e-xpression
            scriptPattern = Pattern.compile("src[\r\n| | ]*=[\r\n| | ]*[\\\"|\\\'](.*?)[\\\"|\\\']", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // Remove any lonesome </script> tag
            scriptPattern = Pattern.compile("</[\r\n| | ]*script[\r\n| | ]*>", Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("");
            // Remove any lonesome <script ...> tag
            scriptPattern = Pattern.compile("<[\r\n| | ]*script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid eval(...) expressions
            scriptPattern = Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid e-xpression(...) expressions
            scriptPattern = Pattern.compile("e-xpression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid javascript:... expressions
            scriptPattern = Pattern.compile("javascript[\r\n| | ]*:[\r\n| | ]*", Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid vbscript:... expressions
            scriptPattern = Pattern.compile("vbscript[\r\n| | ]*:[\r\n| | ]*", Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid onload= expressions
            scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            scriptPattern = Pattern.compile("onerror(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // 新增level3 校验规则
            // scriptPattern = Pattern.compile("<[\\s\\x00]*SCRIPT|<[\\s\\x00]*SVG/ONLOAD=DOCUMENT.LOCATION|<[\\s\\x00]*SVG/ONLOAD=JAVASCRIPT:ALERT|%3C[+\\s\\x00]*SCRIPT|<!--|-->|<[\\s\\x00]*IMG|<[\\s\\x00]*A[\\s\\00]*HREF=|<[\\s\\x00]*HEAD|<[\\s\\x00]*BOY|<[\\s\\x00]*DIV|<[\\s\\x00]*TABLE|<[\\s\\x00]*FRAME|<[\\s\\x00]*IFRAME|<[\\s\\x00]*FRAMESET|<[\\s\\x00]*NOFRAME|<[\\s\\x00]*PLAINTEXT|<[\\s\\x00]*LINK|<[\\s\\x00]*MAP|<[\\s\\x00]*BGSOUND|<[\\s\\x00]*FORM|<[\\s\\\\x00]*INPUT|<[\\s\\x00]*BODY|<[\\s\\\\x00]*SELECT|<[\\s\\x00]*OPTION|<[\\s\\x00]*TEXTAREA|<[\\s\\x00]*APPLET|<[\\s\\x00]*OBJECT|<[\\s\\x00]*EMBED|<[\\s\\x00]*NOSCRIPT|<[\\s\\x00]*STYLE|%3C[+\\s\\x00]*IMG|%3C[+\\s\\x00]*HEAD|%3C[+\\s\\x00]*BOY|%3C[+\\s\\x00]*DIV|%3C[+\\s\\x00]*TABLE|%3C[+\\s\\x00]*FRAME|%3C[+\\s\\x00]*IFRAME|%3C[+\\s\\x00]*FRAMESET|%3C[+\\s\\x00]*NOFRAME|%3C[+\\s\\x00]*PLAINTEXT|%3C[+\\s\\x00]*LINK|%3C[+\\s\\x00]*MAP|%3C[+\\s\\x00]*BGSOUND|%3C[+\\s\\x00]*FORM|%3C[+\\s\\x00]*INPUT|%3C[+\\s\\x00]*SELECT|%3C[+\\s\\x00]*OPTION|%3C[+\\s\\x00]*TEXTAREA|%3C[+\\s\\x00]*APPLET|%3C[+\\s\\x00]*OBJECT|%3C[+\\s\\x00]*EMBED|%3C[+\\s\\x00]*NOSCRIPT|%3C[+\\s\\x00]*STYLE|ALERT[\\s\\x00]*\\(|PROMPT[\\s\\x00]*\\(|CONFIRM[\\s\\x00]*\\(|\\|<([^<>]*)>|((SELECT|FROM|INSERT|DELETE|V$VERSION|WHERE|ROWNUM|CASE|BANNER|THEN|TRUNCATE)([\\s]*)/\\*\\*/)|'\\s*OR.+=|NET USER|XP_CMDSHELL|EXEC MASTER.DBO.XP_CMDSHELL|NET LOCALGROUP ADMINISTRATORS|((AND|OR)(\\s+|')(.+)(=|LIKE)(.+))|('--)|('\\s+--)|(;--)|(;\\s+--)|execute\\sxp_cmdshell|exec\\sxp_cmdshell|@@version|version\\(\\)|database\\(\\)|db_name|IS_SRVROLEMEMBER|table_cursor|sql\\sspace\\scaret|serverproperty|sql\\sserver\\ssubstr|sql\\scast|sql\\svarchar|sql\\slen|sql\\sUnicode|ascii|concat|sql\\ssys_context|sql\\scount|sql\\sasc|sql\\smid|sql\\spack_received|sql\\sbitand|sql\\sconnection_id|sql\\sget_host_name|utl_http.request|waitfor\\sdelay|benchmark|pg_sleep|sysdatabases|sysobjects|all_objects|syscolumns|information_schema\\.tables|information_schema\\.columns|mysql\\.user|v$parameter|sys\\.v$parameter|v$database|sys\\.v$database|v$version|sys\\.v$version|_user|session_role|user_role_priv|user_tables|granted_role|table_schema|ExtractValue|UpdateXml|or\\sgreatest|or\\sisnull|oracle\\schr|oracle\\\\sdbms_pipe|select\\s@@datadir|select\\sinto|load_file\\(.*\\)|backup\\s+\\*\\s+disk\\s+=|[^a-z0-9_]{1}(select|insert|delete|update|drop|sleep|order|and|xor|or|create|case|exec|else|execute|if)[^a-z0-9_]{1}|^(select|insert|delete|update|drop|sleep|order|and|xor|or|create|case|exec|else|execute|if)[^a-z0-9_]{1}");
            Matcher m = scriptPattern.matcher(value);
            /*StringBuffer sb = new StringBuffer();
            log.info("level3 校验规则前2：{}",value);
            while (m.find()) {
                m.appendReplacement(sb, "");
                log.info("level3 校验规则中：{}",sb);
            }
            log.info("level3 校验规则后：{}",sb);
            if (!StringUtils.isEmpty(sb.toString())) {
                value = sb.toString();
            }*/
            if(m.find()){
                value = m.replaceAll("");
            }
        }
        return value;
    }
}
